""" A module to handle nicely conversions between floating point degrees and
    any combination of degrees, minutes, seconds.

    :Author: 2009-2011 Roberto Vidmar

    :Copyright: 2011-2012
                Nicola Creati <ncreati@inogs.it>
                Roberto Vidmar <rvidmar@inogs.it>

    :License: MIT/X11 License (see :download:`license.txt
                               <../../license.txt>`)
"""

import numpy as np
try:
  from __init__ import deprecated
except ImportError:
  def deprecated(ob):
    """ The Identity Decorator
    """
    return ob

def deg2dms(degs, decimals=4):
  """ Return degs converted to (signum, degrees, minutes, seconds.decimals)

    :param degs: degrees
    :type degs: float
    :param decimals: number of decimals
    :type decimals: int
    :returns: (signum, degrees, minutes, seconds.decimals)
    :rtype: tuple
    :raises:
  """
  # Convert to DMS.SS
  dmss = "%d %d %d %.6f" % degDMSfuzz(degs, decimals)
  [signum, degs, mins, secs] = dmss.split()
  return int(signum), int(degs), int(mins), float(secs)

def deg2dm(degs, decimals=6):
  """ Return degs converted to (signum, degrees, minutes.decimals)

    :param degs: degrees
    :type degs: float
    :param decimals: number of decimals
    :type decimals: int
    :returns: (signum, degrees, minutes.decimals)
    :rtype: tuple
    :raises:
  """
  # Convert to DM.MM
  dmm = "%d %d %.6f" % degDMfuzz(degs, decimals)
  [signum, degs, mins] = dmm.split()
  return int(signum), int(degs), float(mins)

@deprecated
def point2DMSString(coords, decimals, separators = ['d', "'", '"']):
  """ For a couple of coords in degrees, return the following strings:

        *[signum] degrees d minutes ' seconds.decimals "*

        *[signum] degrees d minutes ' seconds.decimals "*

    :param coords: (degrees, degrees)
    :type coords: tuple of float
    :param decimals: number of decimals for seconds
    :type decimals: int
    :param separators: tuple of separators (defaults to ['d', "'", '"'])
    :type separators: tuple of characters
    :returns: "[signum] degrees d minutes ' seconds.decimals " +
              "[signum] degrees d minutes ' seconds.decimals "
    :rtype: string
    :raises:

    .. warning:: This function is deprecated, use d2dms instead
  """
  format = ("%%0%d.%df") % (decimals+3, decimals)
  signum, degs, mins, secss = d2dms(coords, decimals)
  if signum > 0:
    ssign=""
  else:
    ssign="-"
  return ("%s%02d%s%02d%s") % (ssign,
           degs[0], separators[0],
           mins[0], separators[1]) + \
         (format) % secss[0] + separators[2] + \
         (" %s%02d%s%02d%s") % (ssign,
           degs[1], separators[0],
           mins[1], separators[1]) + \
         (format) % secss[1] + separators[2]

@deprecated
def point2DMString(coords, decimals, separators = ['d', "'"]):
  """ For a couple of coords in degrees, return the following strings:

        *[signum] degrees d minutes.decimals '*

        *[signum] degrees d minutes.decimals '*

    :param coords: (degrees, degrees)
    :type coords: tuple of float
    :param decimals: number of decimals for minutes
    :type decimals: int
    :param separators: tuple of separators (defaults to ['d', '"'])
    :type separators: tuple of characters
    :returns: "[signum] degrees d minutes.decimals ' +
              "[signum] degrees d minutes.decimals '
    :rtype: string
    :raises:

    .. warning:: This function is deprecated, use d2dm instead
  """
  format = ("%%0%d.%df") % (decimals+3, decimals)
  signum, degs, minsm = deg2dm(coords, decimals)
  if signum > 0:
    ssign = ""
  else:
    ssign = "-"
  return ("%s%02d%s") % (ssign,
           degs[0], separators[0]) + \
         (format) % minsm[0] + separators[1] +\
         (" %s%02d%s") % (ssign,
           degs[1], separators[0]) + \
         (format) % minsm[1] + separators[1]

def num2str(num, decimals=6):
  """ Return num converted to string with "decimals" decimals

    :param num: number
    :type num: float
    :param decimals: number of decimals
    :type decimals: int
    :returns: d.dddddd
    :rtype: string
    :raises: StandardError
  """
  try:
    format = ("%%.%df") % decimals
    return (format) % num
  except StandardError:
    return ""

def divmod(dividend, *divisors):
  """ Return quotient and modulos for the integer division of dividend by
      divisors

    :param dividend: dividend
    :type dividend: float
    :param divisors: variable number of divisors
    :type divisors: integer(s)
    :returns: tuple of quotient and modulo(s)
    :rtype: tuple
    :raises:
  """
  modulos = ()
  q = dividend
  while divisors:
    q, r = q.__divmod__(divisors[-1])
    modulos = (r,) + modulos
    divisors = divisors[:-1]
  return (q, ) + modulos

def secs_to_wdhms(seconds):
  """ Return seconds converted to (week, days, hours, minutes, seconds)

    :param seconds: number of seconds
    :type seconds: float
    :returns: (week, days, hours, minutes, seconds)
    :rtype: tuple
    :raises:
  """
  w, d, h, m, s = divmod(seconds, 7, 24, 60, 60)
  return (w, d, h, m, s)

def d2dms(degs, decimals=4):
  """ Convert degs into degrees, minutes, seconds.decimals

    :param degs: degrees
    :type degs: float or numpy array
    :returns: (degrees, minutes, seconds.decimals)
    :rtype: tuple
    :raises:
  """
  try:
    fuzz = (0.5 / (10 ** decimals)) * np.ones(degs.shape)
  except:
    fuzz = 0.5 / (10 ** decimals)
  absdegs = abs(degs)
  sign = degs / absdegs
  dint, dfrac = divmod(absdegs, 1.0)
  mint, mfrac = divmod(dfrac * 60, 1.0)
  secs = mfrac * 60.0
  try:
    odds = ((secs + fuzz) >= 60.0).nonzero()
    secs[odds] -= fuzz[odds]
  except:
    secs -= fuzz
  return sign * dint, mint, secs

def d2dm(degs, decimals=6):
  """ Convert degs into degrees, minutes.decimals

    :param degs: degrees
    :type degs: float or numpy array
    :returns: (degrees, minutes.decimals)
    :rtype: tuple
    :raises:
  """
  try:
    fuzz = (0.5 / (10 ** decimals)) * np.ones(degs.shape)
  except:
    fuzz = 0.5 / (10 ** decimals)
  absdegs = abs(degs)
  sign = degs / absdegs
  dint, dfrac = divmod(absdegs, 1.0)
  mint, mfrac = divmod(dfrac * 60, 1.0)
  minutes = mint + mfrac
  try:
    odds = ((minutes + fuzz) >= 60.0).nonzero()
    minutes[odds] -= fuzz[odds]
  except:
    minutes -= fuzz
  return sign * dint, minutes

def _degDM(deg):
  """ Degrees to degrees/minutes.

    :param deg: degrees
    :type deg: float
    :returns: (signum, degUnits, minutes)
    :rtype: tuple
    :raises:
  """
  if deg != 0:
    absDeg = abs(deg)
    signum = deg / absDeg
    degUnits, degFraction = divmod (absDeg, 1.0)
    minUnits, minFraction = divmod (degFraction * 60.0, 1.0 )
    minutes = minUnits + minFraction
    #return (signum * degUnits, minutes)
    return (signum, degUnits, minutes)
  else:
    return (1, 0, 0.0)

def _degDMS(deg):
  """ Degrees to degrees/minutes/seconds.

    :param deg: degrees
    :type deg: float
    :returns: (signum, degUnits, minUnits, minutes)
    :rtype: tuple
    :raises:
  """
  if deg != 0:
    absDeg = abs(deg)
    signum = deg / absDeg
    degUnits, degFraction = divmod (absDeg, 1.0 )
    minUnits, minFraction = divmod (degFraction * 60.0, 1.0 )
    seconds = minFraction * 60.0
    return (signum, degUnits, minUnits, seconds)
  else:
    return (1, 0, 0, 0.0)

def dms2deg (*dms):
  """ Degrees / [minutes [ / seconds]] to degrees.

    :param \*dms: degrees
    :type \*dms: float(s) or numpy array
    :returns: degrees
    :rtype: float
    :raises:

    .. warning:: The sign MUST be specified by the first argument.
  """
  sign = np.sign(dms[0])
  if isinstance(sign, np.ndarray):
    sign[sign == 0] = 1
  else:
    if sign == 0:
      sign = 1.

  if len(dms) == 3:
    return sign * (np.abs(dms[0]) +
      (dms[1] / 60.0) + (dms[2] / 3600.0))
  elif len(dms) == 2:
    return sign * (np.abs(dms[0]) + (dms[1] / 60.0))
  else:
    return dms[0]

def degDMSfuzz(deg, secDigits=3):
  """ Convert degrees to deg/min/sec avoiding the *60 seconds* syndrome.

      If we are displaying integral seconds, then seconds values
      whose fraction is in the range [0.5, 1.0) will round up,
      which will make a value like 59.6 display as 60, which is bad.

      So for that case, we can subtract 0.5 seconds from the degree
      value before conversion to avoid the problem.  Similarly,
      if we are displaying seconds with tenths, the trouble range is
      values with fractions in [0.95, 1.0).  If hundredths, the
      trouble range is [0.995, 1.0).  In general, for D digits
      of precision, subtracting (0.5/(10**D)) will prevent the
      *60 seconds* syndrome.

      However, we do not want to apply the fuzz unless it actually
      will cause rounding up.

    :param deg: degrees
    :type deg: float
    :returns: (sign, degrees, minutes, seconds.decimals)
    :rtype: tuple
    :raises:
  """

  #-- 1 --
  # [ fuzz   :=  0.5 / ( 10 ** secDigits )
  #   d,m,s  :=  deg converted to (deg,min,sec) ]
  fuzz = (0.5 / (10 ** secDigits))
  sign, d, m, s  = _degDMS(deg)

  #-- 2 --
  # [ if dms[2] >= (60-fuzz)
  #   else ->
  #     return dms ]
  if  s >= (60 - fuzz):
    #return (d, m, s - fuzz )
    return (sign, d, m, s - fuzz )
  else:
    #return (d, m, s)
    return (sign, d, m, s)

def degDMfuzz (deg, secDigits=5):
  """ Convert degrees to deg/min avoiding the *60 minutes* syndrome.

      If we are displaying integral minutes, then minutes values
      whose fraction is in the range [0.5, 1.0) will round up,
      which will make a value like 59.6 display as 60, which is bad.
      So for that case, we can subtract 0.5 minutes from the degree
      value before conversion to avoid the problem.  Similarly,
      if we're displaying minutes with tenths, the trouble range is
      values with fractions in [0.95, 1.0).  If hundredths, the
      trouble range is [0.995, 1.0).  In general, for D digits
      of precision, subtracting (0.5/(10**D)) will prevent the
      *60 minutes* syndrome.

      However, we do not want to apply the fuzz unless it actually
      will cause rounding up.

    :param deg: degrees
    :type deg: float
    :returns: (sign, degrees, minutes.decimals)
    :rtype: tuple
    :raises:
  """

  #-- 1 --
  # [ fuzz :=  0.5 / ( 10 ** secDigits )
  #   d,m  :=  deg converted to (deg,min) ]
  fuzz = (0.5 / (10 ** secDigits))
  sign, d, m = _degDM(deg)

  #-- 2 --
  # [ if dm[2] >= (60-fuzz)
  #   else ->
  #     return dm ]
  if  m >= (60 - fuzz):
    #return (d, m - fuzz)
    return (sign, d, m - fuzz)
  else:
    #return (d, m)
    return (sign, d, m)
